↜ Back to index Introduction to Numerical Analysis 2
Part b–Lecture 6
In the previous lecture, we started to speed up the conjugate gradient method for sparse matrices.
A matrix is said to be sparse (疎行列) if most of its entries are zero. If a matrix A is sparse, we have an opportunity to save memory necessary to store A since we need to only remember the nonzero entries. Furthermore, we might be able to speed up the computation of the matrix-vector product Ax, (Ax)_i = \sum_{j=1}^n A_{ij}x_j, by not performing the multiplication and addition on the right-hand side whenever A_{ij} is zero.
As we saw in the implementation of lap_mul
in Lecture 5, if the distribution of the nonzero elements in a matrix A is regular, we do not even need to remember the matrix in a variable! But of course life is not always that easy.
Let us consider a sparse matrix A = \begin{pmatrix} 7 & 0 & -3 & 0\\ 2 & 0 & 0 & 0\\ 0 & 4 & 0 & 0\\ 0 & 0 & -2 & 0\\ \end{pmatrix} This matrix has 5 nonzero entries out of the total of 16 entries. Let us consider how we can take an advantage of this fact to speed up the computation of Ax and save some memory.
Our goal is to store the nonzero entries in such a way that we can easily compute the matrix-vector product Ax given as (Ax)_i = \sum_{j=1}^n A_{ij} x_j. Whenever A_{ij} is zero for some i and j we can skip that term. So we need to remember only the values of nonzero entries and the row and column in which they are located. This is enough to compute Ax.
The most straightforward way is to use 3 arrays of size 5:
integer, parameter :: k = 5
real v(k)
integer ri(k), ci(k)
We use v
to store the values of the nonzero elements, and ri
and ci
to store the row and column indices. For example, we can set the values to
= [7., -3., 2., 4., -2.]
v = [1, 1, 2, 3, 4]
ri = [1, 3, 1, 2, 3] ci
This way we only need to remember 3 numbers for each nonzero element of A. This type of sparse matrix storage is called coordinate list.
Exercise 1. What is k
and a possible content of the arrays v
, ri
and ci
for the following matrix?
M = \begin{pmatrix} 0 & 5 & 9 & 0\\ 0 & 0 & 0 & 0\\ -2 & 0 & 0 & -7\\ 0 & 6 & 3 & -8\\ 2 & 0 & 0 & 0 \end{pmatrix}
Let us now try to implement the matrix-vector multiplication Ax given the 3 arrays above.
One implementation, assuming that A is a square matrix, can look like:
function sparse_coo_mul(v, ri, ci, x) result(y)
real v(:), x(:), y(size(x))
integer ri(:), ci(:)
integer i
= 0
y do i = 1,size(v)
= y(ri(i)) + v(i) * x(ci(i))
y(ri(i)) enddo
end
Note that we need to pass all 3 arrays v
, ri
and ci
to specify the matrix.
Exercise 2. Suppose that the size of the matrix is n\times n and has k nonzero elements. What is the number of multiplications and additions of real
values that is performed in sparse_coo_mul
?
We can further improve the storage of a sparse matrix. Here we consider the nonzero elements to be ordered according to their row index. This way the content of the array ri
for matrix M above is
= [1, 1, 3, 3, 4, 4, 4, 5] ri
Note that many of the numbers are repeated. To save space, we can instead just remember which entries belong to the first row, to the second row, and so on. An easy way is to replace ri
by an array rb
of size n + 1, where n is the number of rows, and set rb(i)
to be the index of the first nonzero entry belonging to row i
. If a row i is empty, we set rb(i)
to be equal to rb(i+1)
. We set rb(n+1)
to k + 1. For matrix M we get
= [1, 3, 3, 5, 8, 9] rb
The number of nonzero entries in row i is given as rb(i+1) - rb(i)
. Note that both rb(2)
and rb(3)
are 3. This indicates that row 2 of the matrix has no nonzero entries.
To summarize, this way we can represent the matrix M using the following 3 arrays.
integer, parameter :: k = 8
integer, parameter :: n = 5
real v(k)
integer rb(n + 1), ci(k)
= [5., 9., -2., -7., 6., 3., -8., 2.]
v = [1, 3, 3, 5, 8, 9]
rb = [2, 3, 1, 4, 2, 3, 4, 1] ci
This type of sparse matrix storage is called compressed sparse row.
Once again, we are interested in the matrix-vector product. Using this format is simple.
function sparse_csr_mul(v, rb, ci, x) result(y)
real v(:), x(:), y(size(x))
integer rb(:), ci(:)
integer i,j
do i = 1,size(x)
= 0
y(i) do j = rb(i),rb(i+1) - 1
= y(i) + v(j) * x(ci(j))
y(i) enddo
enddo
end
Check for yourself that the above code computes the correct product for the example matrix above.
Exercise 3. With parameters as in Exercise 2, how many multiplications and additions of real values does sparse_csr_mul
perform? Do not count the additions in rb(i+1) - 1
.
Let us now consider examples of how to initialize the sparse matrix storage for a practical matrix whose size depends on the input value.
Example 1. Write a program that reads n from the input and initializes the coordinate list storage for matrix A given as
A_{ij} :=
\begin{cases}
2, & i =j,\\
-1, & |i - j| = 1,\\
0 & \text{otherwise}.
\end{cases}
It should print the content of v
, rb
and ci
arrays.
implicit none
real, dimension(:), allocatable :: v
integer, dimension(:), allocatable :: rb, ci
integer i, j, n, k, t
read *, n
! number of nonzero elements
= 3 * n - 2
k
allocate(v(k))
allocate(rb(n + 1))
allocate(ci(k))
! t is the index where the next element will be inserted
= 1
t do i = 1, n
= t
rb(i)
if (i > 1) then
= i - 1
ci(t) = -1
v(t) = t + 1
t endif
= i
ci(t) = 2.
v(t) = t + 1
t
if (i < n) then
= i + 1
ci(t) = -1
v(t) = t + 1
t endif
enddo
+ 1) = k + 1
rb(n
print *, 'v', v
print *, 'rb', rb
print *, 'ci', ci
end
$ ./a.out <<< "1"
v 2.00000000
rb 1 2
ci 1
$ ./a.out <<< "2"
v 2.00000000 -1.00000000 -1.00000000 2.00000000
rb 1 3 5
ci 1 2 1 2
$ ./a.out <<< "3"
v 2.00000000 -1.00000000 -1.00000000 2.00000000 -1.00000000 -1.00000000 2.00000000
rb 1 3 6 8
ci 1 2 1 2 3 2 3
Exercises
Exercise 4. Test the code from Example 1 above by comparing the result of the multiplication of a given vector with the expected result for a few different n. Use the function sparse_csr_mul
above. You can use the function lap_mul
from Lecture b5 to find the expected result.
Exercise 5. Implement a program that reads n from the input, and initializes the compressed sparse row storage for the identity matrix. It should print the content of v
, rb
and ci
arrays.
Exercise 6. Write a program that reads n from the input initializes the compressed sparse row for matrix A given as
A_{ij} :=
\begin{cases}
1, & i = j \text{ or } i = n + 1 - j,\\
0 & \text{otherwise}.
\end{cases}
It should print the content of v
, rb
and ci
arrays as in Example 1.
Submit the code to the LMS.
Example n = 2.
$ ./a.out <<< "2"
v 1.00000000 1.00000000 1.00000000 1.00000000
rb 1 3 5
ci 1 2 1 2
Example n = 3.
$ ./a.out <<< "3"
v 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000
rb 1 3 4 6
ci 1 3 2 1 3
Example n = 4.
$ ./a.out <<< "4"
v 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000
rb 1 3 5 7 9
ci 1 4 2 3 2 3 1 4